﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;

using Matchingo.Common.Constants;
using Matchingo.Common.Types.Types;
using Matchingo.Common.Types.EventArguments;
using Matchingo.Common.Types.Interfaces;

namespace Matchingo.Models.ScoreProvider
{    
    public class HighScoreProvider : IHighScoreProvider
    {
        #region ========== Private Constants ==========

        private const string HIGHSCORE_PROVIDER_FILENAME = CommonConstants.FILE_GAME_SCORES;
        private const int LOWEST_GAME_VERSION_SUPPORTED = CommonConstants.GAME_RECORD_VERSION_LOWEST_SUPPORTED;

        #endregion

        #region ========== Public Properties ==========

        private List<Game> _gameRecords;
        public List<Game> GameRecords 
        {
            get { return _gameRecords; }
            private set
            {
                _gameRecords = value;
                NotifyPropertyChanged("GameRecords");
            }
        }

        #endregion

        #region ========== Constructors ===========

        public HighScoreProvider()
        {
            GameRecords = new List<Game>();
        }

        #endregion

        #region ========== Public Methods ==========

        public void Initialize()
        {            
            LoadRecords();
        }

        // NOTE:: we would normally just always save asynchronously, but for some reason the unit tests throw an exception
        // when working with the isolated storage on a background thread.. so unit tests should always save synchronously, not asynch
        public void AddRecord(Game g, bool saveAsynchronously)
        {
            if (g.GameState != Game.GameStateEnum.Ended)
                throw new ArgumentException("HighScoreProvider.AddRecord() called with game which has not yet ended");
            if (g.ArtificialGame)
                throw new ArgumentException("HighScoreProvider.AddRecord() called to save a record played by an artificial player");

            GameRecords.Add(g);
            SaveRecords(saveAsynchronously);

            // if anyone is listening to the RecordAdded event then fire it off
            if (RecordAdded != null)
            {
                RecordAdded(this, new GameRecordAddedEventArgs(g));
            }
        }

        public List<Game> GetHighScoresByCardCount(int topNCount, int cardCount)
        {
            // select top XX GameRecords orderedby Score
            var highScores = (from gr in GameRecords where gr.CardCount == cardCount
                              orderby gr.Score descending
                              select gr).Take(topNCount);

            return highScores.ToList();
        }

        public void LoadRecords()
        {
            using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (file.FileExists(HIGHSCORE_PROVIDER_FILENAME))
                {
                    using (var stream = new IsolatedStorageFileStream(HIGHSCORE_PROVIDER_FILENAME, FileMode.Open, file))
                    {
                        using (StreamReader sr = new StreamReader(stream))
                        {
                            string lineOfData = sr.ReadLine();
                            if (lineOfData == null || lineOfData.Trim() == string.Empty)
                            {
                                GameRecords = new List<Game>();
                            }
                            else
                            {
                                GameRecords = Deserialize<List<Game>>(lineOfData);
                            }
                        }
                    }
                }
            }

            RemoveOutDatedGameRecords();
        }

        #endregion

        #region ========== Private Methods ==========

        private void RemoveOutDatedGameRecords()
        {
            #region sanity checks

            if (GameRecords == null)
            {
                throw new ArgumentException("HighScoreProvider.RemoveOutDatedGameRecords() called while GameRecords list was null");
            }

            if (GameRecords.Count < 1)
            {
                return;
            }

            #endregion
           
            // get a list of all game records which are within the supported version set
            var validGameRecords = (from g in GameRecords
                                    where g.RecordVersion >= LOWEST_GAME_VERSION_SUPPORTED
                                    select g);

            // set this (possible) subset to the GameRecords and resave them - removing all out of date records
            GameRecords = validGameRecords.ToList();
        }

        private void SaveRecords(bool saveAsynchronously)
        {
            SaveRecords(this.GameRecords, saveAsynchronously);
        }

        private static void SaveRecords(List<Game> gameRecords, bool saveAsynchronously)
        {
            if (saveAsynchronously)
            {
                // we want this to be asynchronous so as to prevent UI stutter
                BackgroundWorker worker = new BackgroundWorker();
                worker.WorkerReportsProgress = false;
                worker.WorkerSupportsCancellation = false;
                worker.DoWork += (s, e) => SaveRecords(gameRecords);
                worker.RunWorkerAsync();
            }
            else
            {
                SaveRecords(gameRecords);
            }
        }

        private static void SaveRecords(List<Game> gameRecords)
        {
            using (var isoStore = IsolatedStorageFile.GetUserStoreForApplication())
            {
                // Create new file
                using (var isoStream = new IsolatedStorageFileStream(HIGHSCORE_PROVIDER_FILENAME, FileMode.OpenOrCreate, isoStore))
                {
                    using (StreamWriter writer = new StreamWriter(isoStream))
                    {
                        string allGameRecords = SerializeToJsonString(gameRecords);
                        writer.WriteLine(allGameRecords);
                        writer.Close();
                    }
                }
            }
        }

        private static string SerializeToJsonString(object objectToSerialize)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(objectToSerialize.GetType());
                serializer.WriteObject(ms, objectToSerialize);
                ms.Position = 0;

                using (StreamReader reader = new StreamReader(ms))
                {
                    return reader.ReadToEnd();
                }
            }
        }

        private static T Deserialize<T>( string jsonString )
        {
            using( MemoryStream ms = new MemoryStream( Encoding.Unicode.GetBytes( jsonString ) ) )
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer( typeof( T ) );
                return ( T )serializer.ReadObject( ms );
            }
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged == null)
                return;

            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region ========== Public Events ==========

        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<GameRecordAddedEventArgs> RecordAdded;

        #endregion
    }
}
